﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Common.Utilities.TextMatch
{
	/// <summary>
	/// 
	/// </summary>
	public class DocMatchUtil
	{
		#region fields
		// docId -> title
		private Dictionary<int, string> _DocTitles;
		// docId -> (termId -> freq)
		private Dictionary<int, Dictionary<int, int>> _DocTermFreqs;
		// phrase -> termId 
		private Dictionary<string, int> _Terms;
		// docId -> (docId -> distance)
		private double[,] _Distances;
		#endregion

		#region ctor 
		/// <summary>
		/// 
		/// </summary>
		public DocMatchUtil()
		{
			this._Terms=new Dictionary<string, int>();
			this._DocTitles=new Dictionary<int, string>();
			this._DocTermFreqs=new Dictionary<int, Dictionary<int, int>>();
			this._Distances = null;
		}
		#endregion

		#region init
		/// <summary>
		/// 
		/// </summary>
		/// <param name="title"></param>
		/// <param name="docContent"></param>
		public void AddDocument(string title, string docContent)
		{
			int lastDocID = 0;
			foreach (int docId in this._DocTitles.Keys)
			{
				if (docId > lastDocID)
					lastDocID = docId;
			}
			int newDocId = lastDocID + 1;
			this._DocTitles.Add(newDocId, title);
			this.IndexDocumentContent(newDocId, docContent);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="docId"></param>
		/// <param name="title"></param>
		/// <param name="docContent"></param>
		public void AddDocument(int docId, string title, string docContent)
		{
			if (this._DocTitles.ContainsKey(docId))
				throw new Exception("Doc is already added");
			this._DocTitles.Add(docId, docContent);
			this.IndexDocumentContent(docId, docContent);
		}

		private void IndexDocumentContent(int docId, string docContent)
		{
			docContent = docContent.ToLower();
			string[] phrases = docContent.Split(new char[] { ' ', '\n', '\r' });
			StopWordsUtil stopWordsUtil = new StopWordsUtil(StopWordsUtil.StopWordList.PubmedStopWordList);
			List<string> stopWords = stopWordsUtil.StopWords;
			Dictionary<int, int> termFreqs = new Dictionary<int, int>();
            PorterStemmer porterStemmer = new PorterStemmer();

			foreach (string phrase in phrases)
			{
                if (phrase.Trim().Length <= 3 || phrase.Trim().Length > 10)
                    continue;
                string stem = porterStemmer.StemTerm(phrase);

				if (stem.Trim().Length > 0 && !stopWords.Contains(stem))
				{
					if (this._Terms.ContainsKey(stem))
					{
						int termId = this._Terms[stem];
						if (termFreqs.ContainsKey(termId))
							termFreqs[termId] = termFreqs[termId] + 1;
						else
							termFreqs.Add(termId, 1);
					}
					else
					{
						int termId = this._Terms.Count + 1;
						this._Terms.Add(stem, termId);
						termFreqs.Add(termId, 1);
						Dictionary<int, int> docFreqs = new Dictionary<int, int>();
						docFreqs.Add(docId, 1);
					}
				}
			}
			this._DocTermFreqs.Add(docId, termFreqs);
		}
		#endregion

		#region lookup
		/// <summary>
		/// 
		/// </summary>
		/// <param name="docID1"></param>
		/// <param name="docID2"></param>
		/// <returns></returns>
		public double GetDistanceFor(int docID1, int docID2)
		{
			int x = -1;
			int y = -1;
			int[] docIDs=new int[this._DocTitles.Count];
			this._DocTitles.Keys.CopyTo(docIDs,0);
			for(int i=0;i<docIDs.Length;i++)
			{
				if(docIDs[i]==docID1)
					x = i;
				else if(docIDs[i]==docID2)
					y = i;
			}
			if (x >= 0 && y >= 0)
				return this._Distances[x, y];
			else
				return double.PositiveInfinity;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public Dictionary<int,Dictionary<int,double>> GetDocDocDistances()
		{
			Dictionary<int,Dictionary<int,double>> docDocDistances=new Dictionary<int, Dictionary<int, double>>();
			int[] docIDs=new int[this._DocTitles.Count];
			this._DocTitles.Keys.CopyTo(docIDs,0);
			for (int i = 0; i < docIDs.Length; i++)
			{
				int docID1 = docIDs[i];
				Dictionary<int,double> toDistances=new Dictionary<int, double>();
				for(int k=0;k<docIDs.Length;k++)
				{
					int docID2 = docIDs[k];
					toDistances.Add(docID2, this._Distances[i,k]);
				}
				docDocDistances.Add(docID1,toDistances);
			}
			return docDocDistances;
		}
		#endregion

		#region compute
		/// <summary>
		/// 
		/// </summary>
		public void ComputeDistances()
		{
			this._Distances= new double[this._DocTitles.Count,this._DocTitles.Count];
			int[] docIDs=new int[this._DocTitles.Count];
			this._DocTitles.Keys.CopyTo(docIDs, 0);
			for(int i=0;i<docIDs.Length;i++)
			{
				int docId1 = docIDs[i];
				Dictionary<int, int> termFreqs1 = this._DocTermFreqs[docId1];
				for(int k=i+1;k<docIDs.Length;k++)
				{
					int docId2 = docIDs[k];
					Dictionary<int, int> termFreqs2 = this._DocTermFreqs[docId2];
					this._Distances[i,k] = this.CalculateCosineDistance(termFreqs1, termFreqs2);
					this._Distances[k,i] = this._Distances[i,k];
				}
			}
		}

		#endregion

		#region cosine
		private double CalculateCosineDistance(
			Dictionary<int, int> termFreq1,
			Dictionary<int, int> termFreq2)
		{
			if (termFreq1 == null || termFreq2 == null || termFreq1.Count == 0 || termFreq2.Count == 0)
				return double.PositiveInfinity;

			foreach (int termID1 in termFreq1.Keys)
			{
				if (!termFreq2.ContainsKey(termID1))
				{
					termFreq2.Add(termID1, 0);
				}
			}
			foreach (int termID2 in termFreq2.Keys)
			{
				if (!termFreq1.ContainsKey(termID2))
				{
					termFreq1.Add(termID2, 0);
				}
			}

			double length1 = 0;
			double length2 = 0;

			//sum of vector multiplication
			double svMul = 0;

			foreach (int termID in termFreq1.Keys)
			{
				double freq1 = termFreq1[termID];
				length1 += Math.Pow(freq1, 2);
				double freq2 = termFreq2[termID];
				length2 += Math.Pow(freq2, 2);
				svMul += freq2 * freq1;
			}

			length1 = Math.Sqrt(length1);
			length2 = Math.Sqrt(length2);

			double cosineDistance = svMul / (length1 * length2);
			return cosineDistance;
		}
		#endregion
	}
}
